package cz.vse.po1715.piskorky.umela_inteligence;

import java.awt.Point;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/*******************************************************************************
 * Instance třídy {@code UměláInteligence} představují hru počítače pro piškvorky.
 *   Jsou schopny vypočítat souřadnice políčka, na které bude počítač táhnout.
 *   Stejný algoritmus mohou využít i k nápovědě hráči (funkce napověz)   
 *
 * @author    Vladimír FOREJT
 * @version   0.00.000
 */
public class UměláInteligence implements IUI {
//== KONSTANTNÍ ATRIBUTY TŘÍDY =================================================
    
    /** Znak prázdného políčka */
    public static final char ZNAK_PRÁZDNÉ = '+';
    
    /** Znak hráče */
    public static final char ZNAK_ČLOVĚK = 'o';
    
    /** Znak počítače */
    public static final char ZNAK_POČÍTAČ = 'x';
    
    /** Počet kamenů tvořících piškvorku */
    public static final int PIŠKVORKA = 5;
    
    
    
    
//== PROMĚNNÉ ATRIBUTY TŘÍDY ===================================================
//== STATICKÝ INICIALIZAČNÍ BLOK - STATICKÝ KONSTRUKTOR ========================
//== KONSTANTNÍ ATRIBUTY INSTANCÍ ==============================================
    
    /** Kritéria pro porovnání - koeficient obrany a útoku */
    private final int koefObrany = 1;
    private final int koefÚtoku = 1;
    
    /** Příznak neobsazené piškvorky */
    private final int prázdnáPiškvorka = 0;
    
    /** Příznak neperspektivní piškvorky - oba hráči v ní mají kameny */
    private final int blok = 999999;
    
    /** Počet sloupců a řádků hracího pole */
    private final int početŘádků;
    private final int početSloupců;
    
    /** Celkový počet polí */
    private final int početPolí;
            
    /** Celkový počet piškvorek */      
    private final int početPiškvorek;
    
    /** Maximální počet nadějných tahů */
    private final int maximumNadějných = 8;
    
    /** Maximální počet vyhrávajících tahů */
    private final int maximumVítězných = 3;
    
    /** Index posledního řádku a sloupce */
    private final int posledníŘádek;
    private final int posledníSloupec;
    
    /** Index posledního políčka v piškvorce */
    private final int ppp = PIŠKVORKA - 1;
    
    /** Všechna políčka na hracím poli, uložená za sebou v ArrayListu */
    private final List<Políčko> seznamPolí;
    
    /** Všechny piškvorky hracího pole */
    private final List<Piškvorka> seznamPiškvorek;
    
    /**Hodnoty indexů polí - pro počítač a pro hráče */
    private final int počítač = 0;
    private final int člověk = 1;
    
    
    
//== PROMĚNNÉ ATRIBUTY INSTANCÍ ================================================
    
    /** Nadějné tahy pro jednotlivé hráče - počítač má v poli index hodnotaPočítač,
     * hráč hodnotaHráč
     */
    private int[] početNadějných = new int[2];
    //
    
    private int[][] seznamNadějných = new int[2][maximumNadějných];
    //private List<Integer> seznamNadějnýchČlověk = new ArrayList<Integer> (maximumNadějných);
    //private List<Integer> seznamNadějnýchPočítač = new ArrayList<Integer> (maximumNadějných);
    
    /** Vítězné tahy jednotlivých hráčů */
    private int[] početVítězných = new int[2];
    //private List<Integer> seznamVítěznýchPočítač = new ArrayList<Integer> (maximumVítězných);
    //private List<Integer> seznamVítěznýchČlověk = new ArrayList<Integer> (maximumVítězných);
    private int[][] seznamVítězných = new int[2][maximumVítězných];
    
//== PŘÍSTUPOVÉ METODY VLASTNOSTÍ TŘÍDY ========================================
//== OSTATNÍ NESOUKROMÉ METODY TŘÍDY ===========================================

//##############################################################################
//== KONSTRUKTORY A TOVÁRNÍ METODY =============================================
    /***************************************************************************
     *
     */
    public UměláInteligence(Point rozměrPole) {
        početŘádků = rozměrPole.x;
        početSloupců = rozměrPole.y;
        početPolí = početŘádků * početSloupců;
        početPiškvorek = ((početŘádků - ppp)*početSloupců) + //Svislých +
                         početŘádků*(početSloupců-ppp) + //Vodorovných +
                         2*(početŘádků - ppp)*(početSloupců - ppp); //Šikmých / a \
        posledníŘádek = rozměrPole.x - 1;
        posledníSloupec = rozměrPole.y - 1;
        seznamPolí = new ArrayList<Políčko>(početPolí);
        seznamPiškvorek = new ArrayList<Piškvorka>(početPiškvorek);
        
        připravHracíPole();
    }
//== ABSTRAKTNÍ METODY =========================================================
//== PŘÍSTUPOVÉ METODY VLASTNOSTÍ INSTANCÍ =====================================
//== OSTATNÍ NESOUKROMÉ METODY INSTANCÍ ========================================
    
    /***************************************************************************
     * Metoda vrací souřadnice prvního tahu počítače. Je možné ji volat pouze 
     *      v případě, že počítač začíná hru - vrací tedy souřadnice úplně
     *      prvního tahu na prázdné hrací pole.
     * 
     * @return souřadnice políčka
     */
//    @Override
    @Override
    public Point začniHru() {
        Point tahPočítač = new Point(početŘádků/2, početSloupců/2); 
        int indexTahPočítač = souřadnice2index(tahPočítač);
        seznamPolí.get(indexTahPočítač).setObsah(ZNAK_POČÍTAČ);
        return tahPočítač;
    }

    /***************************************************************************
     * Metoda vrací souřadnice pole, na které počítač umístil svůj znak
     * 
     * @param tahHráče - souřadnice políčka, na které táhl hráč. Na tento tah
     *                  pak počítač vrací souřadnice svého tahu.
     *          souřadnice 1,1 - první řádek první políčko
     *          souřadnice 2,3 - druhý řádek odshora, třetí políčko
     * 
     * @return souřadnice políčka, na které táhne počítač (x: řádek, y: sloupec)
     */
//    @Override
    @Override
    public Point hraj(Point tahHráče) {
        int indexTahPočítač;
        Point tahPočítač;
        int indexTahČlověk = souřadnice2index(tahHráče);
        seznamPolí.get(indexTahČlověk).setObsah(ZNAK_ČLOVĚK);
        analyzujPole(člověk, indexTahČlověk);
        indexTahPočítač = vyberNejlepšíTah(počítač);
        seznamPolí.get(indexTahPočítač).setObsah(ZNAK_POČÍTAČ);
        analyzujPole(počítač, indexTahPočítač);
        tahPočítač = index2souřadnice(indexTahPočítač);
        return tahPočítač;
    }

    /***************************************************************************
     * Metoda @napověz() slouží jako nápověda - vrací souřadnice políčka, které počítač
     *      zvolí jako nejvýhodnější pro hráče
     *      * 
     * @return souřadnice políčka
     */
    @Override
    public Point napověz() {
        int indexTah = vyberNejlepšíTah(člověk);
        return index2souřadnice(indexTah);
    }
    
//== SOUKROMÉ A POMOCNÉ METODY TŘÍDY ===========================================
//== SOUKROMÉ A POMOCNÉ METODY INSTANCÍ ========================================
    
    /***************************************************************************
     * Inicializuje všechna políčka (nastaví úvodní váhu apod.) a charakteristiky
     * jednotlivých piškvorek
     */
    private void připravHracíPole() {
        /** Vytvoření všech polí */
        for (int i = 0; i <= početPolí; i++) {
            seznamPolí.add(new Políčko());
        }
        /** Vytvoření všech piškvorek */
        for (int i = 0; i<= početPiškvorek; i++) {
            seznamPiškvorek.add(new Piškvorka());
        }
        /** Vynulování seznamu nadějných a vítězných tahů */
        početNadějných[člověk] = početNadějných[počítač] = 0;
        početVítězných[člověk] = početVítězných[počítač] = 0;
        
        /** Skupiny piškvorek natočených do jednotlivých směrů */
        SměrPiškvorek vodorovné = new SměrPiškvorek(0, početŘádků, 
                                            0, početSloupců-ppp, 1);
        SměrPiškvorek svislé = new SměrPiškvorek(0, početŘádků-ppp, 
                                            0, početSloupců, početSloupců);
        SměrPiškvorek jihovýchod = new SměrPiškvorek(0, početŘádků-ppp, 
                                            0, početSloupců-ppp, početSloupců+1);
        SměrPiškvorek jihozápad = new SměrPiškvorek(0, početŘádků-ppp, 
                                            ppp, početSloupců, početSloupců-1);
        Collection<SměrPiškvorek> směry = new ArrayList<SměrPiškvorek>(4);
        směry.add(vodorovné);
        směry.add(svislé);
        směry.add(jihovýchod);
        směry.add(jihozápad);        
        /** Cyklus, který nejprve prochází jednotlivé směry natočení, řádky a sloupce pole
         * a nakonec políčka obsažená v piškvorce a přiřazuje piškvorkám políčka,
         * která obsahují a zároveň políčkům piškvorky, které jimi prochází
         */
        // Indexy, které se používají v cyklu
        int indexPiškvorky = 0;
        int indexPole = 0;
        for (SměrPiškvorek směr : směry) {
            for (int řádek = směr.getPočátečníŘádek(); řádek < směr.getKoncovýŘádek(); řádek++) {
                for (int sloupec = směr.getPočátečníSloupec(); sloupec < směr.getKoncovýSloupec(); sloupec++) {
                    
                    for (int polePiškvorky = 0; polePiškvorky < PIŠKVORKA; polePiškvorky++) {
                        /** Nastavení indexu na první políčko piškvorky */
                        //???indexPole = (řádek * početSloupců + sloupec) - 1;
                        indexPole = řádek * početSloupců + sloupec;
                        indexPole += směr.getVzdálenostSouseda()*polePiškvorky;
                        /** Do aktuální piškvorky se přidá index aktuálního políčka */
                        seznamPiškvorek.get(indexPiškvorky).addIndexPolíčka(polePiškvorky, indexPole);
                        /** Do aktuálního políčka se přidá index aktuální piškvorky */
                        seznamPolí.get(indexPole).addIndexPiškvorky(indexPiškvorky);
                        /** Zvýší hodnotu počtu procházejících piškvorek */
                        seznamPolí.get(indexPole).zvyšPočetProchPiškvorek();
                    }
                    indexPiškvorky++;
                }
            }
        }
        /** Nastavení počáteční váhy polí (rovna počtu procházejících piškvorek */
        for (Políčko pole : seznamPolí) {
            pole.setVáha(počítač, pole.getPočetProchPiškvorek());
            pole.setVáha(člověk, pole.getPočetProchPiškvorek());
        }
        
    }
    
    /***************************************************************************
     * Metoda přepočítá váhy polí po tahu jednoho z hráčů
     * 
     * @param hráč: 0 - počítač, 1 - člověk
     * @param indexTaženéhoPole: index políčka v seznamu polí, na které bylo taženo
     */
    private void analyzujPole(int hráč, int indexTaženéhoPole) {
        /** Protihráč jako opak hráče */
        int protihráč = počítač;
        if (hráč == počítač) {
            protihráč = člověk;
        }
        /** Značka majitele piškvorky - pro počítač kladná, pro člověka záporná čísla */
        int q = hráč == člověk ? -1 : 1;
        
        /** Smaže nadějné a vítězné tahy hráče, který táhl */
        početVítězných[hráč] = 0;
        početNadějných[hráč] = 0;
        
        /** Smazání nadějných tahů protihráče
         * Pokud bylo taženo na některé z protihráčových nadějných polí, odebere se 
        *        i se svým párovým polem                    */
        for (int i = 0; i < početNadějných[protihráč]; i++) {
            if(seznamNadějných[protihráč][i] == indexTaženéhoPole) {
                // Odebrání taženého pole (nahrazení indexem posledního pole
                seznamNadějných[protihráč][i] = seznamNadějných[protihráč][(početNadějných[protihráč]-1)];
                početNadějných[protihráč]--;
                if ((i % 2) == 0) { //pole má sudý index
                    seznamNadějných[protihráč][i+1] = seznamNadějných[protihráč][(početNadějných[protihráč])-1];
                } else { //pole má lichý index
                    seznamNadějných[protihráč][i-1] = seznamNadějných[protihráč][(početNadějných[protihráč])-1];
                }
                početNadějných[protihráč]--; //celkový počet nadějných snížen o 2
                break; //------------------------------------------------------>
            }
        }
        
        /** Smazání vítězných tahů protihráče, pokud ho svým tahem hráč zablokoval */
        for (int i = 0; i < početVítězných[protihráč]; i++) {
            if(seznamVítězných[protihráč][i] == indexTaženéhoPole) {
                seznamVítězných[protihráč][i] = seznamVítězných[protihráč][početVítězných[protihráč]];
                početVítězných[protihráč]--;
                break; //------------------------------------------------------>
            }
        }
        
        
        Políčko taženéPole = seznamPolí.get(indexTaženéhoPole);
        //TODO - možná kontrola, jestli už náhodou není obsazené?
        
        //Nejdříve list zasažených piškvorek
        List<Integer> procházejícíPiškvorky = taženéPole.getProchPiškvorky();
        /** Projde všechny piškvorky, které prochází taženým polem */
        for (int indexPiškvorky : procházejícíPiškvorky) {
            Piškvorka procházejícíPiškvorka = seznamPiškvorek.get(indexPiškvorky);
            int[] políčkaPiškvorky = procházejícíPiškvorka.getPolíčka();
            int majitel = procházejícíPiškvorka.getMajitel();
            int početKamenů = Math.abs(majitel);
            
            /** Větvení podle toho, kdo je současným majitelem piškvorky */
            if (majitel == blok) {
                //nedělá se nic, piškvorka je zablokovaná
                
            } else if (majitel == 0) {
                //Zatím nikdo - nastaví majitele piškvorky                
                procházejícíPiškvorka.setMajitel(q);  
                /** Cyklus přes všechna políčka piškvorky. */
                for (int i = 0; i <= ppp; i++) {
                    Políčko políčko = seznamPolí.get(políčkaPiškvorky[i]);
                    políčko.přičtiVáhu(hráč, 2);
                    políčko.přičtiVáhu(protihráč, -1);
                }
                
            } else if ((hráč == počítač && majitel > 0) ||
                    hráč == člověk && majitel < 0) { 
                // Doplňujeme kámen do vlastní piškvorky -> zvýšit váhu kamenů v piškvorce
                
                majitel += q;
                procházejícíPiškvorka.setMajitel(majitel);
                početKamenů = Math.abs(majitel);
                /** Cyklus přes všechna políčka piškvorky. Volným navýší váhu */
                for (int i = 0; i <= ppp; i++) {
                    Políčko políčko = seznamPolí.get(políčkaPiškvorky[i]);
                    if (políčko.getObsah() == ZNAK_PRÁZDNÉ) {
                        políčko.přičtiVáhu(hráč, 2^početKamenů);
                        /** Pokud zbývá už jediné políčko do piškvorky -> zaznamenat */
                        if (početKamenů == PIŠKVORKA - 1) {
                            if (početVítězných[hráč] < maximumVítězných) {
                                seznamVítězných[hráč][početVítězných[hráč]] = políčkaPiškvorky[i];
                                početVítězných[hráč]++;                                
                            }
                        }
                    }
                }
                /** Pokud jsou 3 kameny vedle sebe -> zbylé dva přidat do nadějných */
                if ( (početKamenů == PIŠKVORKA - 2) && 
                  seznamPolí.get(políčkaPiškvorky[0]).getObsah() == ZNAK_PRÁZDNÉ && 
                  seznamPolí.get(políčkaPiškvorky[ppp]).getObsah() == ZNAK_PRÁZDNÉ &&
                  početNadějných[hráč] < maximumNadějných ) {
                    seznamNadějných[hráč][početNadějných[hráč]] = políčkaPiškvorky[0];
                    seznamNadějných[hráč][(početNadějných[hráč]+1)] = políčkaPiškvorky[ppp];
                    početNadějných[hráč] += 2;
                }
                
            } else {
                // Poslední tah byl do cizí piškvorky - zablokovala se
                for (int i = 0; i <= ppp; i++) {
                    // Snížit váhu protihráči - až doteď byla v jeho rukou
                    Políčko políčko = seznamPolí.get(políčkaPiškvorky[i]);
                    políčko.přičtiVáhu(protihráč, -(početKamenů^2));
                }
                procházejícíPiškvorka.setMajitel(blok);
            }
            
        }
        
    }
    
    /***************************************************************************
    * Metoda vypočítá na základě kritérií nejlepší tah a vrátí jeho
    * index v seznamPolí
    * 
    */
    private int vyberNejlepšíTah(int hráč) {
        int indexNejlepšího = blok;
        
        /** pole políček, které se bude předávat metodě porovnej */
        int[] polePorovnávaných;
        
        /** Protihráč jako opak hráče */
        int protihráč = počítač;
        if (hráč == počítač) {
            protihráč = člověk;
        }
        if (početVítězných[hráč]>0) {
            /* Když existuje vítězný tah, beru první na řadě */
            indexNejlepšího = seznamVítězných[hráč][0];
        } else if (početVítězných[protihráč] > 0) {
            /* Bráním protihráči v jeho vítězném */
            indexNejlepšího = seznamVítězných[protihráč][0];
        } else if (početNadějných[protihráč] > 0) {
            /* Zachytit některý z jeho nadějných tahů - metodou porovnej vybrat který */
            polePorovnávaných = new int[početNadějných[protihráč]];
            System.arraycopy(seznamNadějných[protihráč], 0, polePorovnávaných, 0, početNadějných[protihráč]);
            indexNejlepšího = porovnej(hráč, polePorovnávaných);
        } else if (početNadějných[hráč] > 0) {
            /* Existují nadějné tahy - vybrat nejlepší */
            polePorovnávaných = new int[početNadějných[hráč]];
            System.arraycopy(seznamNadějných[hráč], 0, polePorovnávaných, 0, početNadějných[hráč]);
            indexNejlepšího = porovnej(hráč, polePorovnávaných);
        } else {
            /* Projet celé hrací pole a vybrat pole s nejvyšší vahou */
            polePorovnávaných = new int[početPolí];
            for(int i = 0; i < početPolí; i++) {
                polePorovnávaných[i] = i;
            }
            indexNejlepšího = porovnej(hráč, polePorovnávaných);
        }
        return indexNejlepšího; //=============================================>
    }
    
    /***************************************************************************
    * Metoda porovnává váhy polí ze zadaného array a vrací jeho index v seznamu polí
    * 
    */
    private int porovnej (int hráč, int[] polePorovnávaných) {
        /** Protihráč jako opak hráče */
        int protihráč = počítač;
        if (hráč == počítač) {
            protihráč = člověk;
        }
        int nejlepší = blok; //index nejlepšího pole
        //TODO - ošetřit, co když je hodnota blok - nemůže se uložit do arraylistu:-(
        int maximum = 0; //vážený součet váh nejlepšího pole
        
        for (int i=0; i<polePorovnávaných.length; i++) {
            Políčko pole = seznamPolí.get(polePorovnávaných[i]);
            if(pole.getObsah() == ZNAK_PRÁZDNÉ) {
                int váhaPole = pole.getVáha()[hráč]*koefÚtoku + pole.getVáha()[protihráč]*koefObrany;
                if (váhaPole >= maximum) {
                    maximum = váhaPole;
                    nejlepší = polePorovnávaných[i];
                }
            }
        }
        if (maximum == 0) {
            nejlepší = blok;
        }
        return nejlepší; //====================================================>
    }
    
    /***************************************************************************
     *Převede index políčka na souřadnice Point(x,y) 
     */
    public Point index2souřadnice(int index) {
        Point souřadnice = 
          new Point (((index) / početSloupců) + 1, (index % početSloupců) + 1);
        return souřadnice;
    }
    
    /***************************************************************************
     *Převede souřadnice Point(řádek, sloupec) políčka na index
     */
    public int souřadnice2index(Point souřadnice) {
        return (souřadnice.x - 1) * početSloupců + souřadnice.y - 1;
    }
    
//== INTERNÍ DATOVÉ TYPY =======================================================
//== TESTY A METODA MAIN =======================================================
//
//    /***************************************************************************
//     * Testovací metoda.
//     */
//    public static void test()
//    {
//        UměláInteligence inst = new UměláInteligence();
//    }
//    /** @param args Parametry příkazového řádku - nepoužívané. */
//    public static void main( String[] args )  {  test();  }

    
}
